# Standard library
from abc import ABC, abstractmethod
import time
import json
from enum import Enum
from typing import Callable, Any, Optional, Type, List, Tuple
from urllib.parse import urlparse, urlunparse

# Third-party libraries
import torch
import requests
import openai
import anthropic
#import google.generativeai as genai
try:
    from google import genai
except ImportError:
    import google.generativeai as genai

from google.genai import types
#from google.generativeai.types import GenerationConfig

# Local modules
try:
    from .mng_json import json_manager, TroubleSgltn
    from .fetch_models import RequestMode
    from .utils import ImageUtils
except ImportError:
    from mng_json import json_manager, TroubleSgltn
    from fetch_models import RequestMode
    from utils import ImageUtils


class ImportedSgltn:
    """
    This class is temporary to prevent circular imports between style_prompt
    and api_requests modules.
    """
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(ImportedSgltn, cls).__new__(cls)
            cls._instance._initialized = False
        return cls._instance

    def __init__(self):
        if not self._initialized: #pylint: disable=access-member-before-definition
            self._initialized = True
            self._cfig = None
            self.get_imports()

    def get_imports(self):
        """Import and initialize singleton instances from style_prompt"""
        # Guard against re-importing if already done
        if self._cfig is None:
            try:
                from .style_prompt import cFigSingleton
            except ImportError:
                from style_prompt import cFigSingleton
            self._cfig = cFigSingleton

    @property
    def cfig(self):
        """Returns the cFigSingleton instance"""
        if self._cfig is None:
            self.get_imports()
        return self._cfig()

       
class RetryConfig:
    """Configuration for retry behavior"""
    def __init__(
        self,
        max_retries: int = 3,
        base_delay: float = 1.0,
        max_delay: float = 10.0,
        exponential_base: float = 2.0,
        retryable_exceptions: Optional[List[Type[Exception]]] = None,
        retryable_http_status_codes: Optional[List[int]] = None
    ):
        self.max_retries = max_retries
        self.base_delay = base_delay
        self.max_delay = max_delay
        self.exponential_base = exponential_base
        self.retryable_exceptions = retryable_exceptions
        self.retryable_http_status_codes = retryable_http_status_codes or [
            408,  # Request Timeout
            429,  # Too Many Requests
            500,  # Internal Server Error
            502,  # Bad Gateway
            503,  # Service Unavailable
            504   # Gateway Timeout
        ]


class ErrorParser:
    """Extracts standardized error information from various API responses"""
    
    @staticmethod
    def get_error_code(response: Any) -> Optional[int]:
        """
        Extracts error code from various response formats.
        Returns error code if found, None otherwise.
        """
        # Handle HTTP Response objects
        if isinstance(response, requests.Response):
            return response.status_code

        # OpenAI-style errors (and compatible services like OpenRouter)
        if hasattr(response, 'error'):
            error = response.error
            if isinstance(error, dict):
                # Direct error code
                if 'code' in error and isinstance(error['code'], int):
                    return error['code']

                if 'status' in error and isinstance(error['status'], int):
                    return error['status']

                if 'status_code' in error and isinstance(error['status_code'], int):
                    return error['status_code']
                
                # Nested in metadata (like OpenRouter/Google)
                metadata = error.get('metadata', {})
                if metadata and isinstance(metadata.get('raw'), str):
                    try:
                        raw_error = json.loads(metadata['raw'])
                        code = raw_error.get('error', {}).get('code')
                        if isinstance(code, int):
                            return code
                    except (json.JSONDecodeError, AttributeError):
                        pass

        # Anthropic-style responses
        if hasattr(response, 'status_code'):
            return response.status_code

        # Handle raw JSON responses (some services return direct JSON)
        if isinstance(response, dict):
            # Try common error code paths
            paths = [
                ['error', 'code'],
                ['error', 'status_code'],
                ['error', 'status'],
                ['code'],
                ['status_code'],
                ['status']
            ]
            for path in paths:
                value = response
                for key in path:
                    if isinstance(value, dict) and key in value:
                        value = value[key]
                    else:
                        value = None
                        break
                if isinstance(value, int):
                    return value
            if 'error' in response:
                error = response['error']
                if isinstance(error, dict):
                    # Google-specific string error codes mapping
                    google_error_map = {
                        "RESOURCE_EXHAUSTED": 429,
                        "UNAVAILABLE": 503,
                        "DEADLINE_EXCEEDED": 504,
                        "INTERNAL": 500,
                        "UNKNOWN": 500         
                    }
                    if 'code' in error and isinstance(error['code'], str):
                        return google_error_map.get(error['code'], 400)                               

        return None

class RetryHandler:
    """Handles retry logic for API calls"""
    def __init__(self, config: RetryConfig, logger: Any):
        self.config = config
        self.logger = logger
        self.error_parser = ErrorParser()

    def calculate_delay(self, attempt: int) -> float:
        """Calculate delay with exponential backoff"""
        delay = min(
            self.config.base_delay * (self.config.exponential_base ** attempt),
            self.config.max_delay
        )
        return delay

    def should_retry(self, response: Any) -> bool:
        """Determine if the response is retryable"""
        error_code = self.error_parser.get_error_code(response)
        
        if error_code:
            # Check if it's a retryable code
            return error_code in self.config.retryable_http_status_codes 

        # Handle standard exceptions
        if isinstance(response, Exception) and self.config.retryable_exceptions:
            return any(isinstance(response, exc) for exc in self.config.retryable_exceptions)
        
        return False

    def execute_with_retry(self, func: Callable, *args, **kwargs) -> Any:
        """Execute function with retry logic"""
        last_exception = None
        last_error_info = None  # Track the last error information
        self.logger.log_events(f"Maximum tries set to: {self.config.max_retries}",
                               is_trouble=True)
        
        for attempt in range(self.config.max_retries):
            try:
                response = func(*args, **kwargs)

                # For HTTP responses
                if isinstance(response, requests.Response):
                    try:
                        response_json = response.json()
                        if 'error' in response_json:
                            error_code = self.error_parser.get_error_code(response_json)
                            if error_code in self.config.retryable_http_status_codes:
                                last_error_info = response_json['error']  # Store error info
                                delay = self.calculate_delay(attempt)
                                
                                self.logger.log_events(
                                    f"Retryable error detected in response content ({error_code}), "
                                    f"retrying in {delay:.2f} seconds...",
                                    TroubleSgltn.Severity.WARNING,
                                    True
                                )
                                time.sleep(delay)
                                continue
                    except ValueError:
                        pass

                    # Then check status codes
                    if 200 <= response.status_code < 300:
                        return response
                    elif self.should_retry(response):
                        last_error_info = {'status': response.status_code, 'text': response.text}
                        delay = self.calculate_delay(attempt)
                        self.logger.log_events(
                            f"Rate limit or server error {response.status_code}, "
                            f"retrying in {delay:.2f} seconds...",
                            TroubleSgltn.Severity.WARNING,
                            True
                        )
                        time.sleep(delay)
                        continue
                    else:
                        return response

                # For OpenAI/API responses with embedded errors
                error_code = self.error_parser.get_error_code(response)
                if error_code and error_code in self.config.retryable_http_status_codes:
                    last_error_info = response.error if hasattr(response, 'error') else str(response)
                    delay = self.calculate_delay(attempt)
                    self.logger.log_events(
                        f"Rate limit or error detected in API response ({error_code}), "
                        f"retrying in {delay:.2f} seconds...",
                        TroubleSgltn.Severity.WARNING,
                        True
                    )
                    time.sleep(delay)
                    continue

                return response

            except Exception as e:
                last_exception = e
                last_error_info = str(e)  # Store exception info
                
                if not self.should_retry(e):
                    self.logger.log_events(
                        f"Non-retryable error occurred: {str(e)}",
                        TroubleSgltn.Severity.ERROR,
                        True
                    )
                    raise

                delay = self.calculate_delay(attempt)
                self.logger.log_events(
                    f"Attempt {attempt + 1}/{self.config.max_retries} failed. "
                    f"Retrying in {delay:.2f} seconds. Error: {str(e)}",
                    TroubleSgltn.Severity.WARNING,
                    True
                )
                time.sleep(delay)

        # Create a meaningful exception with the last error information
        error_message = f"Maximum retry attempts ({self.config.max_retries}) exceeded. "
        if last_error_info:
            error_message += f"Last error: {last_error_info}"
        
        # Raise the original exception if we have one, otherwise raise a RuntimeError
        if last_exception:
            raise last_exception
        raise RuntimeError(error_message)

class RetryConfigFactory:
    """Factory for creating retry configurations based on request type"""
    
    @staticmethod
    def create_config(request_type: RequestMode) -> RetryConfig:
        web_exceptions = [
            requests.exceptions.Timeout,
            requests.exceptions.ConnectionError,
            requests.exceptions.RequestException,
            ConnectionError,
            TimeoutError
        ]
        
        api_exceptions = [
            openai.APIConnectionError,
            openai.RateLimitError,
            openai.APIStatusError
        ]

        anthropic_exceptions = [
            anthropic.APIConnectionError,
            anthropic.RateLimitError,
            anthropic.APIStatusError,
            anthropic.APIError
        ]

        configs = {
            RequestMode.OPENAI: RetryConfig(
                max_retries=3,
                base_delay=1.0,
                max_delay=10.0,
                retryable_exceptions=api_exceptions
            ),
            RequestMode.CLAUDE: RetryConfig(
                max_retries=2,
                base_delay=2.0,
                max_delay=8.0,
                retryable_exceptions=anthropic_exceptions
            ),
            RequestMode.OPENSOURCE: RetryConfig(
                max_retries=3,
                base_delay=1.0,
                max_delay=8.0,
                retryable_exceptions=web_exceptions,
                retryable_http_status_codes=[408, 429, 500, 502, 503, 504]
            ),
            RequestMode.OSSIMPLE: RetryConfig(
                max_retries=3,
                base_delay=1.0,
                max_delay=8.0,
                retryable_exceptions=web_exceptions,
                retryable_http_status_codes=[408, 429, 500, 502, 503, 504]
            ),
            RequestMode.LMSTUDIO: RetryConfig(
                max_retries=2,
                base_delay=0.5,
                max_delay=4.0,
                retryable_exceptions=web_exceptions,
                retryable_http_status_codes=[408, 429, 500, 502, 503, 504]
            ),
            RequestMode.GROQ: RetryConfig(
                max_retries=3,
                base_delay=1.0,
                max_delay=6.0,
                retryable_exceptions=api_exceptions
            ),
            RequestMode.OOBABOOGA: RetryConfig(
                max_retries=2,
                base_delay=1.0,
                max_delay=6.0,
                retryable_exceptions=web_exceptions,
                retryable_http_status_codes=[408, 429, 500, 502, 503, 504]
            ),
            # DALL-E specific configuration
            RequestMode.DALLE: RetryConfig(
                max_retries=3,
                base_delay=2.0,
                max_delay=15.0,
                retryable_http_status_codes=[400,429],
                retryable_exceptions=[
                    openai.APIConnectionError,
                    openai.RateLimitError,
                    openai.APIStatusError
                ]
            ),

            RequestMode.GPTIMAGEGEN or RequestMode.GPTIMAGEEDIT: RetryConfig(
                max_retries=3,
                base_delay=2.0,
                max_delay=15.0,
                retryable_http_status_codes=[400,429],
                retryable_exceptions=[
                    openai.APIConnectionError,
                    openai.RateLimitError,
                    openai.APIStatusError
                ]
            ),

            RequestMode.GEMINI: RetryConfig(
                max_retries=2,  # As recommended in the docs ("retrying no more than two times")
                base_delay=1.0,  # "The minimum delay is one second"
                max_delay=8.0,   # Allow for exponential backoff
                retryable_exceptions=[
                    # Google API common exceptions
                    ConnectionError,
                    TimeoutError
                ],
                # Based on the documentation, these are the retryable HTTP status codes
                retryable_http_status_codes=[
                    429,  # RESOURCE_EXHAUSTED - quota limits, server overload
                    500,  # INTERNAL - server error/overload
                    503,  # UNAVAILABLE - service temporarily unavailable
                    504   # DEADLINE_EXCEEDED - request timeout
                ]
            )
                    }
        return configs.get(request_type, RetryConfig())
    

class Request(ABC):
    """Abstract base class for all request types"""

    class RequestType(Enum):
        COMPLETION = "completion"
        POST = "post"
        GENERATE = "generate"
        EDIT = "edit_image"
        ANTHROPIC = "claude"
        GEMINI = "genai"

    def __init__(self):
        self.imps = ImportedSgltn()
        self.utils = request_utils()
        self.cFig = self.imps.cfig
        self.mode = RequestMode
        self.j_mngr = json_manager()
        self.img_u = ImageUtils()
        
        # Initialize retry configuration and handler
        retry_config = RetryConfigFactory.create_config(self.cFig.lm_request_mode)
        self.retry_handler = RetryHandler(retry_config, self.j_mngr)

    @property
    def blank_tensor(self):
        return torch.zeros(1, 64, 64, 3, dtype=torch.float32)

    def _initialize_retry_handler(self, **kwargs):
        """Initialize retry handler with optional override from kwargs"""
        # Get base configuration
        retry_config = RetryConfigFactory.create_config(self.cFig.lm_request_mode)
        
        # Override max_retries if provided in kwargs otherwise use default
        if 'tries' in kwargs and kwargs['tries']:
            tries = kwargs['tries']
            if isinstance(tries, str) and tries != "default":
                retry_config.max_retries = int(tries)
               
        self.retry_handler = RetryHandler(retry_config, self.j_mngr)

    def _make_request(self, request_type: RequestType, *args) -> Any:
        """Unified request method handling different request types"""
        if request_type == self.RequestType.COMPLETION:
            client, params = args
            return client.chat.completions.create(**params)
        
        elif request_type == self.RequestType.ANTHROPIC:
            client, params = args
            return client.messages.create(**params)
        
        elif request_type == self.RequestType.GEMINI:
            client, model, content, content_config = args
            response = client.models.generate_content(
                model=model,
                contents=content,
                config=content_config
            )
            # Adapt the response to OpenAI format before processing by execute_with_retry          
            return self.utils.adapt_gemini_to_openai_format(response, model)        
        
        elif request_type == self.RequestType.POST:
            url, headers, params = args
            return requests.post(url, headers=headers, json=params, timeout=(12, 120))
        
        elif request_type == self.RequestType.GENERATE:
            client, params = args
            return client.images.generate(**params)
        
        elif request_type == self.RequestType.EDIT:
            client, params = args
            return client.images.edit(**params)
                    
        else:
            raise ValueError(f"Unsupported request type: {request_type}")        

    @abstractmethod
    def request_completion(self, **kwargs) -> Any:
        pass


    def _log_completion_metrics(self, response: Any, response_type: str = "standard"):
        """Common logging for completion metrics"""
        try:
            if response_type == "standard":
                if getattr(response, 'model', None):
                    self.j_mngr.log_events(
                        f"Using LLM: {response.model}", 
                        is_trouble=True
                    )
                if getattr(response, 'usage', None):
                    self.j_mngr.log_events(
                        f"Tokens Used: {response.usage}",
                        TroubleSgltn.Severity.INFO,
                        True
                    )
            elif response_type == "json":
                if response.get('model'):
                    self.j_mngr.log_events(
                        f"Using LLM: {response['model']}", 
                        is_trouble=True
                    )
                if response.get('usage'):
                    self.j_mngr.log_events(
                        f"Tokens Used: {response['usage']}",
                        TroubleSgltn.Severity.INFO,
                        True
                    )
        except Exception as e:
            self.j_mngr.log_events(
                f"Unable to report completion metrics: {e}",
                TroubleSgltn.Severity.INFO,
                True
            )

class oai_object_request(Request):
    """Concrete class for OpenAI API object-based requests"""
    
    # def _make_completion_request(self, client, params):
    #     """Wrapped completion request for retry handling"""
    #     return client.chat.completions.create(**params)

    def _get_client(self) -> Optional[Any]:
        """Get appropriate client based on request type"""
        request_type = self.cFig.lm_request_mode
        client = None
        error_message = None

        if request_type in [self.mode.OPENSOURCE, self.mode.OLLAMA]:
            if self.cFig.lm_url:
                self.j_mngr.log_events(
                    "Setting client to OpenAI Open Source LLM object",
                    is_trouble=True
                )
                client = self.cFig.lm_client
            else:
                error_message = "Open Source api object is not ready for use, no URL provided."
                
        elif request_type == self.mode.GROQ:
            if self.cFig.lm_url:
                self.j_mngr.log_events(
                    "Setting client to OpenAI Groq LLM object",
                    is_trouble=True
                )
                client = self.cFig.lm_client
            else:
                error_message = "Groq OpenAI api object is not ready for use, no URL provided."

        elif request_type == self.mode.GEMINI:
            if self.cFig.lm_url:
                self.j_mngr.log_events(
                    "Setting client to OpenAI Gemini LLM object",
                    is_trouble=True
                )
                client = self.cFig.lm_client
            else:
                error_message = "Groq OpenAI api object is not ready for use, no URL provided."                
                
        elif request_type == self.mode.OPENAI:
            if self.cFig.key:
                self.j_mngr.log_events(
                    "Setting client to OpenAI ChatGPT object",
                    is_trouble=True
                )
                client = self.cFig.openaiClient
            else:
                error_message = "Invalid or missing OpenAI API key. Keys must be stored in an environment variable."

        if error_message:
            self.j_mngr.log_events(
                error_message,
                TroubleSgltn.Severity.WARNING,
                True
            )

        return client

    def request_completion(self, **kwargs) -> str:
        """Execute completion request with retry handling"""
        GPTmodel = kwargs.get('model')
        creative_latitude = kwargs.get('creative_latitude', 0.7)
        tokens = kwargs.get('tokens', 500)
        prompt = kwargs.get('prompt', "")
        instruction = kwargs.get('instruction', "")
        #file = kwargs.get('file', "").strip()
        image = kwargs.get('image', None)
        example_list = kwargs.get('example_list', [])
        add_params = kwargs.get('add_params', None)

        CGPT_response = ""
        client = self._get_client()
        self._initialize_retry_handler(**kwargs)

        if not client:
            return "Unable to process request, client initialization failed"

        # Build messages based on presence of image
        if image is not None:
            messages = self.utils.build_data_multi(prompt, instruction, example_list, image)            
        else:
            messages = self.utils.build_data_basic(prompt, example_list, instruction)

        # Handle empty input case
        if not any([prompt, instruction, example_list]) and image is None:
            return "Photograph of a stained empty box with 'NOTHING' printed on its side in bold letters"

        params = {
            "model": GPTmodel,
            "messages": messages,
            "temperature": creative_latitude,
            "max_tokens": tokens  
        }

        # Certain models have parameter restrictions  
        if self.cFig.lm_request_mode != RequestMode.GEMINI:   
            params = self.utils.model_param_adjust(params, self.cFig.lm_request_mode)
            
        if add_params:
            self.j_mngr.append_params(params, add_params, ['param', 'value'])

        try:
            response = self.retry_handler.execute_with_retry(
                self._make_request,
                self.RequestType.COMPLETION,
                client,
                params
            )  #_make_request is passed as a wrapped function, the arguments that follow are passed into
               #args which is unpacked as a tuple in _make_request()

            if response and response.choices and 'error' not in response:
                self._log_completion_metrics(response)
                CGPT_response = self.utils.clean_response_text(
                    response.choices[0].message.content
                )
            else:
                err_mess = getattr(response, 'error', "Error message missing")
                self.j_mngr.log_events(
                    f"Server was unable to process this request. Error: {err_mess}",
                    TroubleSgltn.Severity.ERROR,
                    True
                )
                CGPT_response = "Server was unable to process the request"

        except Exception as e:
            self.j_mngr.log_events(
                f"Request failed: {str(e)}",
                TroubleSgltn.Severity.ERROR,
                True
            )
            CGPT_response = "Server was unable to process the request"

        return CGPT_response

class claude_request(Request):
    """Concrete class for Claude/Anthropic API requests"""
    
    def request_completion(self, **kwargs) -> str:
        claude_model = kwargs.get('model')
        creative_latitude = kwargs.get('creative_latitude', 0.7)
        tokens = kwargs.get('tokens', 500)
        prompt = kwargs.get('prompt', "")
        instruction = kwargs.get('instruction', "")
        image = kwargs.get('image', None)
        example_list = kwargs.get('example_list', [])
        add_params = kwargs.get('add_params', None)

        claude_response = ""
        client = self.cFig.anthropic_client
        self._initialize_retry_handler(**kwargs)

        if not client:
            self.j_mngr.log_events(
                "Invalid or missing Anthropic API key. Keys must be stored in an environment variable.",
                TroubleSgltn.Severity.ERROR,
                True
            )
            return "Invalid or missing Anthropic API key"

        # Build messages
        messages = self.utils.build_data_claude(prompt, example_list, image)

        # Handle empty input case
        if not any([prompt, instruction, example_list]) and image is None:
            return "Empty request, no input provided"

        # Prepare request parameters
        params = {
            "model": claude_model,
            "messages": messages,
            "temperature": creative_latitude,
            "system": instruction,
            "max_tokens": tokens
        }

        if add_params:
            self.j_mngr.append_params(params, add_params, ['param', 'value'])

        try:
            response = self.retry_handler.execute_with_retry(
                self._make_request,
                self.RequestType.ANTHROPIC,
                client,
                params
            )

            if response and 'error' not in response:
                self._log_completion_metrics(response)
                try:
                    claude_response = response.content[0].text
                    claude_response = self.utils.clean_response_text(claude_response)
                except (IndexError, AttributeError):
                    claude_response = "No valid data was returned"
                    self.j_mngr.log_events(
                        "Claude response was not valid data",
                        TroubleSgltn.Severity.WARNING,
                        True
                    )
            else:
                claude_response = "Server was unable to process the request"
                self.j_mngr.log_events(
                    'Server was unable to process this request.',
                    TroubleSgltn.Severity.ERROR,
                    True
                )

        except Exception as e:
            error_msg = self.utils.parse_anthropic_error(e)
            self.j_mngr.log_events(
                f"Request failed: {error_msg}",
                TroubleSgltn.Severity.ERROR,
                True
            )
            claude_response = "Server was unable to process the request"

        return claude_response
    
class genaiRequest(Request):

    class CompletionMode(Enum):
        TEXT = ['text',]             # Standard text completion
        TEXT_IMAGE = ['text','image',]   # Multimodal completion that can return text and/or images

    class CompletionAction(Enum):
        CLIENT = 1
        POST = 2
            
    def request_completion(self, **kwargs):
        model = kwargs.get('model', 'gemini-pro')
        prompt = kwargs.get('prompt', '')
        instruction = kwargs.get('instruction', '')
        tokens = kwargs.get('tokens', 1200)
        creative_latitude = kwargs.get('creative_latitude', 0.7)
        image = kwargs.get('image', None)
        example_list = kwargs.get('example_list',[])
        add_params = kwargs.get('add_params', [])
        completion_mode = kwargs.get('completion_mode', self.CompletionMode.TEXT.value)
        #completion_action = kwargs.get('completion_action', self.CompletionAction.CLIENT)
        
        key = self.cFig.custom_key or self.cFig.gemini_key
        content = self.utils.build_gemini_content(prompt, example_list, image) #U

        gen_config_params = {
            "max_output_tokens": tokens,
            "temperature": creative_latitude,
            "response_modalities": completion_mode
        }
        if instruction:
            gen_config_params['system_instruction'] = instruction

        if add_params:
            self.j_mngr.append_params(gen_config_params,add_params, ["param","value"])

                # Parameter name mapping for Gemini API if needed
        self.utils.model_param_adjust(gen_config_params,RequestMode.GEMINI)    
        
        try:
            # Create the content config with generation_config and response_modalities
            content_config = types.GenerateContentConfig(
                **gen_config_params
            )
            
            # Log the configuration being used
            self.j_mngr.log_events(
                f"Using GenerateContentConfig with response_modalities={completion_mode}",
                TroubleSgltn.Severity.INFO,
                False
            )
            
            # Set up client
            client = genai.Client(api_key=key)

            # Call execute_with_retry
            openai_format_response = self.retry_handler.execute_with_retry(
                self._make_request,
                self.RequestType.GEMINI,
                client,
                model,
                content,
                content_config
                )
            
            # Log metrics using the adapted format
            self._log_completion_metrics(openai_format_response, "json")

            if "choices" in openai_format_response:
                finish_reasons = [choice.get('finish_reason', "N/A") for choice in openai_format_response["choices"]]
                # Now you have a list of all finish reasons
                self.j_mngr.log_events(f"Reasons for the inference finishing: {', '.join(finish_reasons)}", is_trouble=True)   
                
            # Check if there was an error
            if "gemini_error" in openai_format_response:
                error_msg = str(openai_format_response["gemini_error"])
                self.j_mngr.log_events(
                    f"Gemini response error: {error_msg}",
                    TroubleSgltn.Severity.ERROR,
                    True
                )
                return f"Error: {error_msg}"

            # Check if there was a safety block
            if "gemini_safety_block" in openai_format_response:
                block_reason = openai_format_response["gemini_safety_block"]["block_reason"]
                block_message = openai_format_response["gemini_safety_block"]["block_reason_message"]
                self.j_mngr.log_events(
                    f"Gemini response blocked: {block_reason} - {block_message}",
                    TroubleSgltn.Severity.ERROR,
                    True
                )
                return f"Error: Content blocked - {block_message}"
            
                    
            # Get text content if available
            text_content = ""
            if openai_format_response["choices"]:
                text_content = openai_format_response["choices"][0]["message"]["content"]
            
            # Return based on completion mode
            if completion_mode == self.CompletionMode.TEXT.value:
                # Text only mode - return just the text content
                return {"text": text_content}
                
            if completion_mode == self.CompletionMode.TEXT_IMAGE.value:
                # Text and image mode - return appropriate structure based on what's available
                if openai_format_response["images"]:
                    return {
                        "text": text_content,
                        "images": openai_format_response["images"]
                    }
                else:
                    return {
                        "text": text_content,
                        "images": {"tensor": self.blank_tensor, "count":0}
                    }
                    
            
            # Default fallback (shouldn't reach here if enum is used properly)
            return {"text": text_content,
                    "images": {"tensor": self.blank_tensor, "count":0}}
        except Exception as e:
            self.j_mngr.log_events(f"An Error occurred when processing the Gemini Completion request.  This process was not completed. Error: {e}",
                                   TroubleSgltn.Severity.ERROR,
                                   True)
            return {
                "text": f"Error: {e}",
                "images": {"tensor": self.blank_tensor, "count":0}
            }


class oai_web_request(Request):
    """Concrete class for OpenAI-compatible web requests"""

    def request_completion(self, **kwargs) -> str:
        GPTmodel = kwargs.get('model', "")
        creative_latitude = kwargs.get('creative_latitude', 0.7)
        url = kwargs.get('url', None)
        tokens = kwargs.get('tokens', 500)
        image = kwargs.get('image', None)
        prompt = kwargs.get('prompt', None)
        instruction = kwargs.get('instruction', "")
        example_list = kwargs.get('example_list', [])
        add_params = kwargs.get('add_params', None)

        CGPT_response = ""
        request_type = self.cFig.lm_request_mode
        self._initialize_retry_handler(**kwargs)

        # URL setup and validation
        self.cFig.lm_url = url
        if not self.cFig.is_lm_server_up:
            self.j_mngr.log_events(
                "Local or remote server is not responding, may be unable to send data.",
                TroubleSgltn.Severity.WARNING,
                True
            )

        # Process image if present
        if image is not None and request_type == self.mode.OSSIMPLE:
            self.j_mngr.log_events(
                "The AI Service using 'Simplified Data' can't process an image. The image will be disregarded in generated output.",
                TroubleSgltn.Severity.INFO,
                True
            )
            image = None

        # Get appropriate key for request type
        key = self._get_key_for_request_type(request_type)
        headers = self.utils.build_web_header(key, request_type)

        # Build message structure
        if request_type == self.mode.OSSIMPLE or image is None:
            messages = self.utils.build_data_basic(prompt, example_list, instruction)
            self.j_mngr.log_events(
                "Using Basic data structure",
                TroubleSgltn.Severity.INFO,
                True
            )
        else:
            messages = self.utils.build_data_multi(prompt, instruction, example_list, image)
            self.j_mngr.log_events(
                "Using Complex data structure",
                TroubleSgltn.Severity.INFO,
                True
            )

        params = {
            "model": GPTmodel,
            "messages": messages,
            "temperature": creative_latitude,
            "max_tokens": tokens  
        }

        # Certain models have parameter restrictions        
        params = self.utils.model_param_adjust(params, request_type)

            
        if add_params:
            self.j_mngr.append_params(params, add_params, ['param', 'value'])

        try:
            response = self.retry_handler.execute_with_retry(
                self._make_request,
                self.RequestType.POST,
                url,
                headers,
                params
            )

            if response.status_code in range(200, 300):
                response_json = response.json()
                if response_json and 'error' not in response_json:
                    CGPT_response = self.utils.clean_response_text(
                        response_json['choices'][0]['message']['content']
                    )
                    self._log_completion_metrics(response_json, "json")
                else:
                    error_message = response_json.get('error', 'Unknown error')
                    self.j_mngr.log_events(
                        f"Server error in response: {error_message}",
                        TroubleSgltn.Severity.ERROR,
                        True
                    )
                    CGPT_response = "Server was unable to process the request"
            else:
                self.j_mngr.log_events(
                    f"Server error status: {response.status_code}: {response.text}",
                    TroubleSgltn.Severity.ERROR,
                    True
                )
                CGPT_response = "Server was unable to process the request"

        except Exception as e:
            self.j_mngr.log_events(
                f"Request failed: {str(e)}",
                TroubleSgltn.Severity.ERROR,
                True
            )
            CGPT_response = "Server was unable to process the request"

        return CGPT_response

    def _get_key_for_request_type(self, request_type: RequestMode) -> str:
        """Get appropriate key based on request type"""
        if request_type == self.mode.OPENAI:
            return self.cFig.key
        elif request_type in [self.mode.OPENSOURCE, self.mode.LMSTUDIO]:
            key = self.cFig.custom_key or self.cFig.lm_key #Will populate with first 'truthy' value
            return key
        elif request_type == self.mode.GROQ:
            return self.cFig.groq_key
        elif request_type == self.mode.GEMINI:
            return self.cFig.gemini_key
        return ""

class ooba_web_request(Request):
    """Concrete class for Oobabooga web requests"""

    def request_completion(self, **kwargs) -> str:
        GPTmodel = kwargs.get('model', "")
        creative_latitude = kwargs.get('creative_latitude', 0.7)
        url = kwargs.get('url', None)
        tokens = kwargs.get('tokens', 500)
        prompt = kwargs.get('prompt', None)
        instruction = kwargs.get('instruction', "")
        example_list = kwargs.get('example_list', [])
        add_params = kwargs.get('add_params', None)

        CGPT_response = ""
        request_type = self.cFig.lm_request_mode
        self._initialize_retry_handler(**kwargs)

        # URL setup and validation
        url = self.utils.validate_and_correct_url(url)
        self.cFig.lm_url = url

        if not self.cFig.is_lm_server_up:
            self.j_mngr.log_events(
                "Local server is not responding, may be unable to send data.",
                TroubleSgltn.Severity.WARNING,
                True
            )

        # Get appropriate key
        key = self.cFig.key if request_type == self.mode.OPENAI else self.cFig.lm_key
        headers = self.utils.build_web_header(key)

        # Build messages with Oobabooga-specific format
        messages = self.utils.build_data_ooba(prompt, example_list, instruction)

        # Prepare request parameters
        params = {
            "model": GPTmodel,
            "messages": messages,
            "temperature": creative_latitude,
            "max_tokens": tokens,
        }

        # Add Oobabooga-specific parameters
        if request_type == self.mode.OOBABOOGA:
            self.j_mngr.log_events(
                f"Processing Oobabooga http: POST request with url: {url}",
                is_trouble=True
            )
            params.update({
                "user_bio": "",
                "user_name": ""
            })

        if add_params:
            self.j_mngr.append_params(params, add_params, ['param', 'value'])

        try:
            response = self.retry_handler.execute_with_retry(
                self._make_request,
                self.RequestType.POST,
                url,
                headers,
                params
            )

            if response.status_code in range(200, 300):
                response_json = response.json()
                if response_json and 'error' not in response_json:
                    CGPT_response = self.utils.clean_response_text(
                        response_json['choices'][0]['message']['content']
                    )
                    self._log_completion_metrics(response_json, "json")
                else:
                    error_message = response_json.get('error', 'Unknown error')
                    self.j_mngr.log_events(
                        f"Server error in response: {error_message}",
                        TroubleSgltn.Severity.ERROR,
                        True
                    )
            else:
                CGPT_response = "Server was unable to process the request"
                self.j_mngr.log_events(
                    f"Server error status: {response.status_code}: {response.text}",
                    TroubleSgltn.Severity.ERROR,
                    True
                )

        except Exception as e:
            self.j_mngr.log_events(
                f"Request failed: {str(e)}",
                TroubleSgltn.Severity.ERROR,
                True
            )
            CGPT_response = "Server was unable to process the request"

        return CGPT_response

class dall_e_request(Request):
    """Concrete class for DALL-E image generation requests"""

    def __init__(self):
        super().__init__()
        self.trbl = TroubleSgltn()
        #self.iu = ImageUtils()
        # Override with DALL-E specific retry config
        retry_config = RetryConfigFactory.create_config(self.cFig.lm_request_mode)
        self.retry_handler = RetryHandler(retry_config, self.j_mngr)


    def request_completion(self, **kwargs) -> Tuple[torch.Tensor, str]:
        GPTmodel = kwargs.get('model')
        prompt = kwargs.get('prompt')
        image_size = kwargs.get('image_size')
        image_quality = kwargs.get('image_quality')
        style = kwargs.get('style')
        batch_size = kwargs.get('batch_size', 1)

        self.trbl.set_process_header('Dall-e Request')
        batched_images = self.blank_tensor
        revised_prompt = "Image and mask could not be created"

        client = self.cFig.openaiClient
        self._initialize_retry_handler(**kwargs)

        if not client:
            self.j_mngr.log_events(
                "OpenAI API key is missing or invalid. Key must be stored in an environment variable.",
                TroubleSgltn.Severity.WARNING,
                True
            )
            return batched_images, revised_prompt

        self.j_mngr.log_events(
            f"Talking to Dalle model: {GPTmodel}",
            is_trouble=True
        )

        images_list = []
        have_rev_prompt = False

        for _ in range(batch_size):
            params = {
                "model": GPTmodel,
                "prompt": prompt,
                "size": image_size,
                "quality": image_quality,
                "style": style,
                "n": 1,
                "response_format": "b64_json"
            }

            try:
                response = self.retry_handler.execute_with_retry(
                    self._make_request,
                    self.RequestType.GENERATE,
                    client,
                    params
                )

                if response and 'error' not in response:
                    if not have_rev_prompt:
                        revised_prompt = response.data[0].revised_prompt
                        have_rev_prompt = True

                    b64Json = response.data[0].b64_json
                    if b64Json:
                        png_image, _ = self.img_u.b64_to_tensor(b64Json)
                        images_list.append(png_image)
                    else:
                        self.j_mngr.log_events(
                            f"Dalle-e could not process an image in your batch of: {batch_size}",
                            TroubleSgltn.Severity.WARNING,
                            True
                        )

            except Exception as e:
                self.j_mngr.log_events(
                    f"Failed to generate image {_ + 1}/{batch_size}: {str(e)}",
                    TroubleSgltn.Severity.ERROR,
                    True
                )

        if images_list:
            count = len(images_list)
            self.j_mngr.log_events(
                f'{count} images were processed successfully in your batch of: {batch_size}',
                is_trouble=True
            )
            batched_images = torch.cat(images_list, dim=0)
        else:
            self.j_mngr.log_events(
                f'No images were processed in your batch of: {batch_size}',
                TroubleSgltn.Severity.WARNING,
                is_trouble=True
            )

        self.trbl.pop_header()
        return batched_images, revised_prompt
    

class gpt_image_request(Request):
    """Concrete class for gpt-image-1 image generation requests"""

    def __init__(self):
        super().__init__()
        self.trbl = TroubleSgltn()
        #self.iu = ImageUtils()
        # Override with DALL-E specific retry config
        retry_config = RetryConfigFactory.create_config(self.cFig.lm_request_mode)
        self.retry_handler = RetryHandler(retry_config, self.j_mngr)


    def request_completion(self, **kwargs) -> Tuple[torch.Tensor, str]:
        GPTmodel = kwargs.get('model')
        prompt = kwargs.get('prompt',"")
        image_size = kwargs.get('image_size')
        image_quality = kwargs.get('image_quality')
        batch_size = kwargs.get('batch_size', 1)
        tries = kwargs.get('tries', 1) #pylint: disable=unused-variable
        image = kwargs.get('image', None)
        mask = kwargs.get('mask', None)

        self._initialize_retry_handler(**kwargs) #tries is used in this function, so ignore unused variable warning

        #self.trbl.set_process_header('GPT Image Request')
        batched_images = self.blank_tensor

        client = self.cFig.openaiClient

        if not client:
            self.j_mngr.log_events(
                "OpenAI API key is missing or invalid. Key must be stored in an environment variable.",
                TroubleSgltn.Severity.WARNING,
                True
            )
            return batched_images
        

        self.j_mngr.log_events(
            f"Talking to GPT model: {GPTmodel}",
            is_trouble=True
        )

        images_list = []
        request_type = self.RequestType.GENERATE #initialize to a default value
        #many parameters appear in the documentation but fail in use.  These are commented out.
        params = { "model": GPTmodel,
            "prompt": prompt,
            "size": image_size,
            "n": 1
        }    
        #moderation: "auto", "low"
        #response_format: "b64_json"
        #output_format: "png","jpeg","webp"   
        #background: "auto", "transparent"     

        ## Handle image and mask if provided.  Neither Image tensor nor maks can be batched.
        ## Both are forced to have alpha channels.
        if self.cFig.lm_request_mode == RequestMode.GPTIMAGEEDIT:

            if image is not None:
                request_type = self.RequestType.EDIT
                # Seems only one image can be processed, no batches
                if image.ndim == 4 and image.size(0) == 1:
                    params['image'] = self.img_u.tensor_to_bytes(image,True)

                elif image.ndim == 4 and image.size(0) > 1:
                    self.j_mngr.log_events(f"{image.size(0)} input images detected. Only one image can be input at a time.  The first image is the only one that will be processed.",
                                            TroubleSgltn.Severity.WARNING,
                                            True)
                    params['image'] = self.img_u.tensor_to_bytes(image[0],True)

            if mask is not None:  #Only one mask allowed at this point, no batches.

                if mask.ndim == 4 and mask.size(0) == 1:
                    params['mask'] = self.img_u.tensor_to_bytes(mask, True, 'mask.png')

                elif mask.ndim == 4 and mask.size(0) > 1:
                    self.j_mngr.log_events(f"{mask.size(0)} masks detected. Only one mask can be input at a time.  The mask(s) will be ignored.",
                                            TroubleSgltn.Severity.WARNING,
                                            True)
                    
        #Quality only works with the image generation request type, not with Edit
        if request_type == self.RequestType.GENERATE:
            params['quality'] = image_quality

        response = None                            

        for i in range(batch_size):
            self.j_mngr.log_events(f"Processing batch image {i + 1}/{batch_size}", is_trouble=True, indent_level=(2,TroubleSgltn.ApplyLevel.NEXT))  

            try:
                response = self.retry_handler.execute_with_retry(
                    self._make_request,
                    request_type,
                    client,
                    params
                )

                #response_json = response.json()

                if response and 'error' not in response:
  
                    b64Json = response.data[0].b64_json
                    #Convert the image to a tensor
                    if b64Json:
                        png_image, _ = self.img_u.b64_to_tensor(b64Json)
                        images_list.append(png_image)
                    else:
                        self.j_mngr.log_events(
                            f"GPT_image could not process an image in your batch of: {batch_size}",
                            TroubleSgltn.Severity.WARNING,
                            True
                        )

                if response and 'error' not in response:
                    self._log_completion_metrics(response)      
                
                self.j_mngr.log_events(f"Batch image #{i+1} complete.",is_trouble=True, indent_level=(1,TroubleSgltn.ApplyLevel.NEXT))

            except Exception as e:
                self.j_mngr.log_events(
                    f"Failed to generate image {i + 1}/{batch_size}: {str(e)}",
                    TroubleSgltn.Severity.ERROR,
                    True,
                    indent_level=(1,TroubleSgltn.ApplyLevel.NEXT)
                )



        if images_list:
            count = len(images_list)
            self.j_mngr.log_events(
                f'{count} images were processed successfully in your batch of: {batch_size}',
                is_trouble=True
            )
            batched_images = torch.cat(images_list, dim=0)
        else:
            self.j_mngr.log_events(
                f'No images were processed in your batch of: {batch_size}',
                TroubleSgltn.Severity.WARNING,
                is_trouble=True
            )

        #self.trbl.pop_header()

        return batched_images 
    

class ImagenRequest(Request):
    """Concrete class for Google Imagen image generation requests"""

    def __init__(self):
        super().__init__()
        self.trbl = TroubleSgltn()

        retry_config = RetryConfigFactory.create_config(self.cFig.lm_request_mode)
        self.retry_handler = RetryHandler(retry_config, self.j_mngr)

    def request_completion(self, **kwargs) -> Tuple[torch.Tensor, str]:
        model = kwargs.get('model')
        prompt = kwargs.get('prompt')
        number_of_images =kwargs.get('number_of_images', 1)
        aspect_ratio = kwargs.get('aspect_ratio', "1:1")


        batched_images = self.blank_tensor
        image_list = []
        key = self.cFig.custom_key or self.cFig.gemini_key
        client = genai.Client(api_key=key)

        params = {
                    "number_of_images": number_of_images,
                    "aspect_ratio": aspect_ratio,
                    "output_mime_type": "image/png"
        }

        try:
            response = client.models.generate_images(
                model=model,
                prompt=prompt,
                config=types.GenerateImagesConfig(**params)
            )
            self.j_mngr.log_events(f"Processing images using model: {model}", is_trouble=True)
        except Exception as e:
            self.j_mngr.log_events(
                    f"Failed to generate Imagen image, Error: {e}",
                    TroubleSgltn.Severity.ERROR,
                    True
                )
            
        if response and  'error' not in response:  #Google has stripped the imagen response object of anything except the image data.             
            if not response.generated_images:  
                self.j_mngr.log_events("No images were generated.  Check the prompt and try again.  This may be due to safety issues",
                                        TroubleSgltn.Severity.WARNING,
                                        True)
                return batched_images
            
            for gen_image in response.generated_images:
                tensor = self.img_u.bytes_to_tensor(gen_image.image.image_bytes)
                image_list.append(tensor)

            if image_list:
                count = len(image_list)
                self.j_mngr.log_events(
                    f'{count} images were processed successfully in your batch of: {number_of_images}',
                    is_trouble=True
                )
                batched_images = torch.cat(image_list, dim=0)

            use_data = []
            # Extract usage information
            if hasattr(response, "usage_metadata"):
                usage_metadata = response.usage_metadata
                if hasattr(usage_metadata, "prompt_token_count"):
                    use_data.append(f"Prompt Token Count: {usage_metadata.prompt_token_count}")
                if hasattr(usage_metadata, "candidates_token_count"):
                    use_data.append(f"Output Token Count{usage_metadata.candidates_token_count}")
                if hasattr(usage_metadata, "total_token_count"):
                    use_data.append(f"Total Token Count: {usage_metadata.total_token_count}")    
            
            self.j_mngr.log_events(f"Token Usage: {', '.join(use_data)}")
        else:
            err_mess = getattr(response, 'error', "Error message missing")
            self.j_mngr.log_events(
                f'No images were processed.  Error: {err_mess}',
                TroubleSgltn.Severity.WARNING,
                is_trouble=True
            )
        return batched_images


class ollama_unload_request(Request):
    """Concrete class for model unload requests"""

    class ModelTTL(Enum):
        KILL = 0
        INDEF = -1
        NOSET = "no_setting"

    def __init__(self):
        super().__init__()
        self.trbl = TroubleSgltn()

    def request_completion(self, **kwargs) -> bool:

        req_mode = self.cFig.lm_request_mode

        keep_alive = kwargs.get('model_TTL', self.ModelTTL.NOSET)
        if not isinstance(keep_alive, self.ModelTTL):
            self.j_mngr.log_events("Invalid `model_TTL` value provided.", 
                                    TroubleSgltn.Severity.WARNING, 
                                    True
            )
            return False        

        if keep_alive == self.ModelTTL.NOSET: #Don't change the current TTL setting
            return True
        
        self.trbl.set_process_header("Ollama Unload Model Setting")
        
        if req_mode not in {RequestMode.OLLAMA, RequestMode.OPENSOURCE}:
            self.j_mngr.log_events("Model Unloading does not work with this AI Service type.",
                                   TroubleSgltn.Severity.WARNING,
                                   True)
            return False        

        model = kwargs.get('model')

        if not model:
            self.j_mngr.log_events("No model specified for unload", 
                                    TroubleSgltn.Severity.WARNING,
                                    True)
            return False
        
        llm_url = kwargs.get('url', 'http://localhost:11434')  # Get URL or use default           
        # replace the URL path with Ollama's native endpoint
        base_url = self.utils.validate_and_correct_url(llm_url, '/api/generate')
        headers = self.utils.build_web_header()

        params = {
            "model": model,
            "keep_alive": keep_alive.value
        }
        try:
            self.j_mngr.log_events(f"Attempting to set model TTL using URL: {base_url}", is_trouble=True)
            response = requests.post(base_url, headers=headers, json=params, timeout=5)

        except requests.RequestException as e:
            self.j_mngr.log_events(f"Model unload request failed: {e.__class__.__name__}: {str(e)}", 
                                    TroubleSgltn.Severity.WARNING, 
                                    True)
            return False
        
        response_text = response.text if response.text else "None Provided"

        if response.status_code == 200:
            self.j_mngr.log_events(f"Model unload setting successful.  Response: {response_text}", is_trouble=True)
            return True
        
        self.j_mngr.log_events(f"Model unload failed with status: {response.status_code}, Response: {response_text}", 
                            TroubleSgltn.Severity.WARNING,
                            True)
        return False
    
    
class request_context:
    def __init__(self)-> None:
        self._request = None
        self.j_mngr = json_manager()

    @property
    def request(self)-> Request:
        return self._request

    @request.setter
    def request(self, request:Request)-> None:
        self._request = request

    def execute_request(self, **kwargs):
        if self._request is not None:
            return self._request.request_completion(**kwargs)
        
        self.j_mngr.log_events("No request strategy object was set",
                               TroubleSgltn.Severity.ERROR,
                               True)
        return None
    
class request_utils:

    def __init__(self)-> None:
        self.j_mngr = json_manager()
        self.mode = RequestMode
        self.img_u = ImageUtils()
        self.imps = ImportedSgltn()

    def model_param_adjust(self, params: dict, request_mode: RequestMode) -> dict:
        """
        Adjusts parameters based on API-specific requirements.
        
        Args:
            params (dict): The parameters dictionary to adjust
            request_mode (RequestMode): The API mode (OPENAI, CLAUDE, GEMINI, etc.)
            
        Returns:
            dict: The adjusted parameters dictionary
        """
        # Handle OpenAI models with specific restrictions
        if request_mode == self.mode.OPENAI:
            # Check if this is an o1 or o3 model - would need to extract model name from params
            model = params.get('model', '')
            adj_models = ['o1', 'o3']
            if any(m in model for m in adj_models):
                self.j_mngr.log_events(
                    "The 'o' models have parameter restrictions. Removing 'max_tokens' and setting 'temperature' to 1",
                    TroubleSgltn.Severity.INFO,
                    True
                )
                
                # Handle temperature parameter
                if 'temperature' in params:
                    params['temperature'] = 1
                
                # Handle max_tokens parameter
                if 'max_tokens' in params:
                    params.pop('max_tokens', None)
        
        # Handle Google Generative AI parameters
        elif request_mode == self.mode.GEMINI:
            # Map parameter names to Gemini-specific ones
            if 'max_tokens' in params:
                params['max_output_tokens'] = params.pop('max_tokens')
            
            # Any other Gemini-specific parameter adjustments here
        
        # Handle Claude parameters
        elif request_mode == self.mode.CLAUDE:
            # Any Claude-specific parameter adjustments
            pass
        
        return params


    def build_data_multi(self, 
                         prompt: str, 
                         instruction: str = "", 
                         examples: list = None, 
                         image: torch.Tensor | str | list[torch.Tensor] | list[str] = None):
        """
        Builds a list of message dicts, aggregating 'role:user' content into a list under 'content' key.
        Supports multiple images.
        
        - image: A single Base64-encoded string or a list of them.
        - prompt: String to be included as 'text' type content under 'user' role.
        - examples: List of additional example dicts to be included.
        - instruction: Instruction string to be included under 'system' role.
        """
        messages = []
        user_role = {"role": "user", "content": None}
        user_content = []

        if instruction:
            messages.append({"role": "system", "content": instruction})         

        if examples:
            messages.extend(examples)

        if prompt:
            user_content.append({"type": "text", "text": prompt})

        processed_images = self.process_image(image)  # Now supports multiple images
        if processed_images:
            if isinstance(processed_images, list):
                user_content.extend(processed_images)  # Add multiple images
            else:
                user_content.append(processed_images)  # Add single image

        if user_content:
            user_role['content'] = user_content   
            messages.append(user_role)

        return messages

    
    def build_data_basic(self, prompt:str, examples:list=None, instruction:str=""):
        """
        Builds a list of message dicts, presenting each 'role:user' item in its own dict.
        - prompt: String to be included as 'text' type content under 'user' role.
        - examples: List of additional example dicts to be included.
        - instruction: Instruction string to be included under 'system' role.
        """

        messages = []

        if instruction:
            messages.append({"role": "system", "content": instruction})

        if examples:
            messages.extend(examples)     
        
        if prompt:
            messages.append({"role": "user", "content": prompt})


        return messages
    
    def build_data_ooba(self, prompt:str, examples:list=None, instruction:str="")-> list:
        """
        Builds a list of message dicts, presenting each 'role:user' item in its own dict.
        Since Oobabooga's system message is broken it includes it in the prompt
        - prompt: String to be included as 'text' type content under 'user' role.
        - examples: List of additional example dicts to be included.
        - instruction: Instruction string to be included under 'system' role.
        """

        messages = []

        ooba_prompt = ""

        if instruction:
            ooba_prompt += f"INSTRUCTION: {instruction}\n\n"

        if prompt:
            ooba_prompt += f"PROMPT: {prompt}"


        if examples:
            messages.extend(examples)
        
        if ooba_prompt:
            messages.append({"role": "user", "content": ooba_prompt.strip()})

        return messages 



    def build_data_claude(self, 
                          prompt: str, 
                          examples: list = None, 
                          image: torch.Tensor | str | list[torch.Tensor] | list[str] = None) -> list:
        """
        Builds a list of message dicts, aggregating 'role:user' content into a list under 'content' key.
        Supports multiple images.
        
        - image: A single Base64-encoded string or a list of them.
        - prompt: String to be included as 'text' type content under 'user' role.
        - examples: List of additional example dicts to be included.
        """
        messages = []
        user_role = {"role": "user", "content": None}
        user_content = []

        if examples:
            messages.extend(examples)

        processed_images = self.process_image(image, RequestMode.CLAUDE)
        if processed_images:
            if isinstance(processed_images, list):
                user_content.extend(processed_images)  # Add multiple images
            else:
                user_content.append(processed_images)  # Add single image

        if prompt:
            user_content.append({"type": "text", "text": prompt})

        if user_content:     
            user_role['content'] = user_content    
            messages.append(user_role)

        return messages

    
    def build_gemini_content(self,
                        prompt: str,
                        examples: list = None,
                        image: torch.Tensor | str | list[torch.Tensor] | list[str] = None,
                        instruction: str=""):
        """
        Builds Gemini-compatible content, aggregating all inputs and examples.
        Maintains logical ordering of examples and content.
        
        For Gemini 2.0+ models, system instructions are included in the content array
        with a "system" role instead of being returned separately.
        """
        # Build conversation history
        conversation = []
        
        # Add system instruction if present
        if instruction:
            conversation.append({
                "role": "system", 
                "parts": [{"text": instruction}]
            })
        
        # Process examples into conversation history
        if examples:
            for example in examples:
                role = example.get("role")
                content = example.get("content")
                if role and content:
                    if role == "user":
                        gemini_role = "user"
                    elif role == "assistant":
                        gemini_role = "model"
                    else:
                        continue  # skip if not user or assistant
                    
                    if isinstance(content, str):
                        conversation.append({"role": gemini_role, "parts": [{"text": content}]})
        
        # Add final user turn with prompt and/or image
        if prompt or image is not None:
            # Process images using the consolidated processor with GEMINI mode
            processed_images = self.process_image(image, RequestMode.GEMINI) if image is not None else None
            
            # Create parts for the user message
            user_parts = []
            
            # Add text prompt if present
            if prompt:
                user_parts.append({"text": prompt})
            
            # Add images if present
            if processed_images is not None:
                if isinstance(processed_images, list):
                    for img_dict in processed_images:
                        user_parts.append({"inline_data": img_dict})
                else:
                    # Single image dictionary case
                    user_parts.append({"inline_data": processed_images})
            
            # Add to conversation as the final user turn
            conversation.append({"role": "user", "parts": user_parts})
        
        # Return the conversation with system instruction integrated
        return conversation

    
    def process_image(self, 
                    image: torch.Tensor | str | list[torch.Tensor] | list[str], 
                    request_type: RequestMode = RequestMode.OPENAI):
        """
        Processes image input (tensor, Base64 string, or list) into the proper request format.

        Args:
            image (torch.Tensor, str, or list): A raw tensor, Base64 string, or a list of either.
            request_type (RequestMode): Determines the API format (Claude, OpenAI, Gemini, etc.).

        Returns:
            dict or list: A processed image dictionary (for a single image) or a list (for multiple).
        """
        if image is None:
            self.j_mngr.log_events("No input image was received")
            return None

        # **Case 1: If image is a tensor, convert it to Base64**
        if isinstance(image, torch.Tensor):  
            N = self.img_u.extract_batch_size(image)  # Extract batch size
            self.j_mngr.log_events(f"Image size extracted as: {N}")
            # Convert each tensor to Base64 using self.img_u.tensor_to_b64()
            base64_images = [self.img_u.tensor_to_base64(image[i]) for i in range(N)]

            # Recursively process the Base64 images
            return self.process_image(base64_images, request_type)

        # **Case 2: If it's a list, process each item recursively**
        if isinstance(image, list):
            # Check if we have mixed types (both tensors and strings), which is likely an error
            if not all(isinstance(img, (torch.Tensor, str)) for img in image):
                self.j_mngr.log_events("Error: List contains unsupported types.", TroubleSgltn.Severity.ERROR, True)
                return None  # Or raise an exception if preferred

            # Recursively process each item (whether tensor or Base64 string)
            processed_list = [self.process_image(img, request_type) for img in image]
            # Filter out None values
            return [item for item in processed_list if item is not None]

        # **Case 3: If it's already a Base64 string, return in correct format based on request type**
        if isinstance(image, str):
            if request_type == self.mode.CLAUDE:
                return {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": "image/png",
                        "data": image
                    }
                }
            elif request_type == self.mode.GEMINI:
                return {
                    "mime_type": "image/png",
                    "data": image
                }
            else:  # Default to OpenAI format
                return {
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:image/png;base64,{image}"
                    }
                }

        self.j_mngr.log_events("Image file is invalid.", TroubleSgltn.Severity.WARNING, True)
        return None
    
    def process_image_gemini(self,
                    image: torch.Tensor | str | list[torch.Tensor] | list[str],
                    request_type: RequestMode = None):
        """
        Processes image input (tensor, Base64 string, or list) into Gemini-compatible format.
        """
        if image is None:
            self.j_mngr.log_events("No input image was received")
            return None

        if isinstance(image, torch.Tensor):
            N = self.img_u.extract_batch_size(image)
            self.j_mngr.log_events(f"Image size extracted as: {N}")
            base64_images = [self.img_u.tensor_to_base64(image[i]) for i in range(N)]
            self.j_mngr.log_events(f"Base64 Image Preview: {base64_images[:1]}", TroubleSgltn.Severity.INFO)
            return self.process_image(base64_images)

        if isinstance(image, list):
            if not all(isinstance(img, (torch.Tensor, str)) for img in image):
                self.j_mngr.log_events("Error: List contains unsupported types.", TroubleSgltn.Severity.ERROR, True)
                return None
            return [self.process_image(img) for img in image]

        if isinstance(image, str):
            return {
                "mimeType": "image/png",
                "data": image
            }

        self.j_mngr.log_events("Image file is invalid.", TroubleSgltn.Severity.WARNING, True)
        return None


    def convert_tensor_batch_to_iterable(self, tensor_batch: torch.Tensor, ensure_alpha:bool=False, iterable_type:str ="list") -> list|tuple: 
        """
        Converts a batch of image tensors into a list of .png files.

        Args:
            tensor_batch (torch.Tensor): A batch of image tensors in [N, H, W, C] format.

        Returns:
            list: A list of BytesIO objects containing .png images.
        """
        if tensor_batch.ndim != 4:
            raise ValueError("Expected tensor batch in [N, H, W, C] format.")

        png_list = []
        for i in range(tensor_batch.size(0)):  # Iterate over the batch dimension
            single_tensor = tensor_batch[i]
            file_name = f"image_{i}.png"  # Generate a unique file name for each image
            png_image = self.img_u.tensor_to_bytes(single_tensor, ensure_alpha=ensure_alpha, file_name=file_name)  # Convert to .png
            png_list.append(png_image)

        if iterable_type == 'tuple': #Can expand if other iterable types are needed
            return tuple(png_list)

        return png_list
    

    def build_web_header(self, key:str="", request_type:RequestMode=None):
        if key:

            if request_type == RequestMode.GEMINI:
                headers = {
                            "Content-Type": "application/json",
                            "x-goog-api-key": key
                        }
                return headers
            
            headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {key}" 
            }
        else:
            headers = {
                "Content-Type": "application/json"
            }    

        return headers    

    def validate_and_correct_url(self, user_url:str, required_path:str='/v1/chat/completions'):
        """
        Takes the user's url and make sure it has the correct path for the connection
        args:
            user_url (str): The url to be validated and corrected if necessary
            required_path (str):  The correct path
        return:            
            A string with either the original url if it was correct or the corrected url if it wasn't
        """
        corrected_url = ""
        parsed_url = urlparse(user_url)
                
        # Check if the path is the required_path
        if not parsed_url.path == required_path:
            corrected_url = urlunparse((parsed_url.scheme,
                                        parsed_url.netloc,
                                        required_path,
                                        '',
                                        '',
                                        ''))

        else:
            corrected_url = user_url
            
        self.j_mngr.log_events(f"URL was validated and is being presented as: {corrected_url}",
                            TroubleSgltn.Severity.INFO,
                            True)

        return corrected_url   
    
    def clean_response_text(self, text: str)-> str:
        # Replace multiple newlines or carriage returns with a single one
        #cleaned_text = re.sub(r'\n+', '\n', text).strip()

        #I used to need more extensive cleanup back when these models first came out.
        #Now model output is much less problematic, so a simplifed cleaner.
        cleaned_text = text.strip()
        return cleaned_text
    
    @staticmethod
    def parse_anthropic_error(e):
        """
        Parses error information from an exception object.

        Args:
            e (Exception): The exception from which to parse the error information.

        Returns:
            str: A user-friendly error message.
        """
        # Default error message
        default_message = "An unknown error occurred"

        # Check if the exception has a response attribute and it can be converted to JSON
        if hasattr(e, 'response') and callable(getattr(e.response, 'json', None)):
            try:
                error_details = e.response.json()
                # Navigate through the nested dictionary safely
                return error_details.get('error', {}).get('message', default_message)
            except ValueError:
                # JSON decoding failed
                return f"Failed to decode JSON from response: {e.response.text}"
            except Exception as ex:
                # Catch-all for any other issues that may arise
                return f"Error processing the error response: {str(ex)}"
        elif hasattr(e, 'message'):
            return e.message
        else:
            return str(e)
        
    def adapt_gemini_to_openai_format(self, gemini_response, model="gemini-pro"):
        """
        Adapts a Google Gemini API response to match the structure of an OpenAI API response.
        This allows existing OpenAI-compatible methods to work with Gemini responses.
        
        Args:
            gemini_response: The response object from the Google Generative AI client
            model: The model name used in the request (fallback if not in response)
                
        Returns:
            A dictionary structured like an OpenAI response
        """

        # Create a base structure that mimics OpenAI response format
        openai_format = {
            "id": getattr(gemini_response, "response_id", "unknown"),
            "object": "chat.completion",
            "created": int(time.time()),
            "model": getattr(gemini_response, "model_version", model),
            "choices": [],
            "usage": {
                "prompt_tokens": 0,
                "completion_tokens": 0,
                "total_tokens": 0
            },
            "images": []  # Add images array for multimodal responses
        }
        
        image_tensors = []
        message_content =""
        # Extract text content and images from candidates
        if hasattr(gemini_response, "candidates") and gemini_response.candidates:
            for i, candidate in enumerate(gemini_response.candidates):
                if hasattr(candidate, "content") and candidate.content:
                    # Extract text from content parts
                    if hasattr(candidate.content, "parts"):
                        for part in candidate.content.parts:
                            if hasattr(part, "text") and part.text:
                                message_content += part.text
                            # Check for image data
                            elif hasattr(part, "inline_data") and part.inline_data:
                                inline_data = part.inline_data
                                if (hasattr(inline_data, "data") and inline_data.data
                                    and isinstance(inline_data.data, bytes)):
                                    image_tensor = self.img_u.bytes_to_tensor(inline_data.data)
                                    image_tensors.append(image_tensor)
                    
                # Create a choice object in OpenAI format
                finish_reason = getattr(candidate, "finish_reason", None) if hasattr(candidate, "finish_reason") else None
                choice = {
                    "index": getattr(candidate, "index", i) if hasattr(candidate, "index") else i,
                    "message": {
                        "role": "assistant",
                        "content": message_content
                    },
                    "finish_reason": self.translate_finish_reason(finish_reason)
                }
                openai_format["choices"].append(choice)
        else:
            # For simple responses with just .text property
            if hasattr(gemini_response, "text"):
                choice = {
                    "index": 0,
                    "message": {
                        "role": "assistant",
                        "content": gemini_response.text
                    },
                    "finish_reason": "stop"
                }
                openai_format["choices"].append(choice)

        # Add combined image tensors to the OpenAI proxy object        
        if image_tensors:
            cat_tensor = self.img_u.pad_images_to_batch(image_tensors)
            openai_format['images'] = {
                "tensor": cat_tensor,
                "count": len(image_tensors)
            }
            self.j_mngr.log_events(
                f"{len(image_tensors)} image(s) generated by Gemini and converted from binary to tensor.",
                is_trouble=True
            )
        
        # Extract usage information
        if hasattr(gemini_response, "usage_metadata"):
            usage_metadata = gemini_response.usage_metadata
            if hasattr(usage_metadata, "prompt_token_count"):
                openai_format["usage"]["prompt_tokens"] = usage_metadata.prompt_token_count
            if hasattr(usage_metadata, "candidates_token_count"):
                openai_format["usage"]["completion_tokens"] = usage_metadata.candidates_token_count
            if hasattr(usage_metadata, "total_token_count"):
                openai_format["usage"]["total_tokens"] = usage_metadata.total_token_count
        
        # Handle errors - directly include the native error structure
        if hasattr(gemini_response, "error"):
            openai_format["gemini_error"] = gemini_response.error
        
        # Check if there was a safety block - include native format
        if hasattr(gemini_response, "prompt_feedback") and hasattr(gemini_response.prompt_feedback, "block_reason") and gemini_response.prompt_feedback.block_reason:
            openai_format["gemini_safety_block"] = {
                "block_reason": gemini_response.prompt_feedback.block_reason,
                "block_reason_message": getattr(gemini_response.prompt_feedback, "block_reason_message", ""),
                "safety_ratings": getattr(gemini_response.prompt_feedback, "safety_ratings", [])
            }
        
        return openai_format
        


    def translate_finish_reason(self, gemini_finish_reason):
        """Extracts the finish reason from Gemini API response"""
        if not gemini_finish_reason:
            return "Uknown"  # Default value
        
        # Extract string value if it's an enum
        if hasattr(gemini_finish_reason, 'value'):
            finish_reason = gemini_finish_reason.value
            if finish_reason.lower() == "stop":
                finish_reason = "Normal Completion"
            else:
                finish_reason = f"Warning, there were problems with this inference: {finish_reason}"
            return finish_reason

        # If it's already a string, return it directly
        return gemini_finish_reason
